namespace hotupdate {
namespace cframework
{
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using Newtonsoft.Json;
    using UnityEngine.Networking;
    using wnity.zframework;

    public static class HotUpdateDllMgr
    {
        public enum Status
        {
            none,
            initing,
            init_failed,
            inited,
            version_checking,
            version_check_failed,
            version_checked,
            bundle_downloading,
            bundle_download_failed,
            bundle_downladed,
            code_loading,
            code_load_failed,
            code_loaded,
        };

        static Status status;
        public const string dllVersionsFileName = "dll_versions.txt";
        public const string hotupdate_code_dir = "hotupdate_code";
        public const string bundle_name = "code_bundle";
        public const string config_file_name = "config.txt";
        static string force_info_file_name = "force_info.txt";
        static readonly string externalDirPath = CommonUtil.cachePath + "/" + hotupdate_code_dir;

        static HotUpdateCodeInfo localInfo = null;
        static HotUpdateCodeInfo externalInfo = null;
        static string externalBundlePath = null;
        static bool useExternal = false;

        static HotUpdateCodeInfo latestInfo = null;
        static int downloadTotalSize;



        static string latestHotUpdateCodeFileName;
        static bool latestIsForce;
        static string host;


        public delegate bool LoadMetadataForAOTAssemblyHandler(byte[] a);
        public static LoadMetadataForAOTAssemblyHandler loadMetadataForAOTAssemblyHandler;
        static AssetBundle codeBundle;


        static object downloadRequest;
        public static Status GetStatus()
        {
            return status;
        }


        public static int GetLatestVersion()
        {

            return latestVersionInfo != null ? latestVersionInfo.hotupdate_version : 0;
        }


        public static HotUpdateCodeInfo latestVersionInfo
        {
            get
            {
                return latestInfo ?? (useExternal ? externalInfo : localInfo);
            }
        }


        public static bool isUsingExternal
        {
            get
            {
                return useExternal;
            }
        }


        static bool ParseVersionInfoByFilePath(string filePath, ref int version, ref int minAppVersion, ref long timestamp, ref int size, ref string md5)
        {
            string f = System.IO.Path.GetFileName(filePath);
            string[] infos = f.Split('_');
            if (infos.Length < 5)
            {
                return false;
            }

            version = int.Parse(infos[0]);
            minAppVersion = int.Parse(infos[1]);
            timestamp = long.Parse(infos[2]);
            size = int.Parse(infos[3]);
            md5 = infos[4];
            return true;
        }


        public static IEnumerator init(string host)
        {
            if (status != Status.none)
            {
                yield return false;
            }
            status = Status.initing;

            HotUpdateDllMgr.host = host;


            string forceVersionFileName = CommonUtil.fileToString(externalDirPath + "/" + force_info_file_name);
            bool externalForce = false;

            HotUpdateCodeInfo tempExternalInfo = null;
            string tempExternalBundlePath = null;
            bool tempUseExternal = false;

            if (System.IO.Directory.Exists(externalDirPath))
            {
                string[] bundleFilePaths = System.IO.Directory.GetFiles(externalDirPath, "*", System.IO.SearchOption.TopDirectoryOnly);
                foreach (var p in bundleFilePaths)
                {
                    //not check force_info_file_name
                    if (System.IO.Path.GetFileName(p) == force_info_file_name)
                    {
                        CommonUtil.Delete(p);
                        continue;
                    }

                    int version = 0;
                    long timestamp = 0;
                    int min_app_version = 0;
                    int size = 0;
                    string md5 = null;

                    if (ParseVersionInfoByFilePath(p, ref version, ref min_app_version, ref timestamp, ref size, ref md5))
                    {
                        if (size != CommonUtil.GetFileSize(p))
                        {
                            continue;
                        }

                        externalForce = forceVersionFileName == System.IO.Path.GetFileName(p);
                        if (tempExternalBundlePath == null || version > tempExternalInfo.hotupdate_version || (version == tempExternalInfo.hotupdate_version && timestamp > tempExternalInfo.build_timestamp) || externalForce)
                        {
                            tempExternalBundlePath = p;

                            if (tempExternalInfo == null)
                            {
                                tempExternalInfo = new HotUpdateCodeInfo();
                            }

                            tempExternalInfo.hotupdate_version = version;
                            tempExternalInfo.min_app_hotupdate_version = min_app_version;
                            tempExternalInfo.build_timestamp = timestamp;

                            if (externalForce)
                            {
                                break;
                            }
                        }
                    }
                }

                foreach (var p in bundleFilePaths)
                {
                    if (p != tempExternalBundlePath && System.IO.Path.GetFileName(p) != force_info_file_name)
                    {
                        CommonUtil.Delete(p);
                    }
                }
            }



            string configPath = UnityWebRequestManager.AddFilePrefixIfNeed(Application.streamingAssetsPath + "/" + hotupdate_code_dir + "/" + config_file_name);

            var request = UnityWebRequest.Get(configPath);
            request.timeout = UnityWebRequestManager.timeout;
            yield return request.SendWebRequest();

            if (request.result == UnityWebRequest.Result.Success)
            {
                string str = System.Text.Encoding.UTF8.GetString(request.downloadHandler.data);
                string configStr = str;
                if (!string.IsNullOrEmpty(configStr))
                {
                    localInfo = JsonConvert.DeserializeObject(configStr, typeof(HotUpdateCodeInfo)) as HotUpdateCodeInfo;
                }
            }
            else
            {
                yield return false;
            }

            if (tempExternalInfo != null && localInfo != null)
            {
                if (tempExternalInfo.hotupdate_version > localInfo.hotupdate_version || (tempExternalInfo.hotupdate_version == localInfo.hotupdate_version && tempExternalInfo.build_timestamp > localInfo.build_timestamp) || externalForce)
                {
                    tempUseExternal = true;
                }

            }

            externalInfo = tempExternalInfo;
            externalBundlePath = tempExternalBundlePath;
            useExternal = tempUseExternal;

            status = Status.inited;
            yield return true;

        }

        public static IEnumerator CheckLatestVersion()
        {
            if (status != Status.inited && status != Status.version_check_failed)
            {
                yield return false;
            }
            status = Status.version_checking;

            if (latestHotUpdateCodeFileName != null)
            {
                yield return false;
            }


            string url = $"{host}/{hotupdate_code_dir}/{dllVersionsFileName}?a=" + Random.Range(0, int.MaxValue);

            var request = UnityWebRequest.Get(url);
            request.timeout = UnityWebRequestManager.timeout;
            yield return request.SendWebRequest();


            if (request.result == UnityWebRequest.Result.Success)
            {
                string str = System.Text.Encoding.UTF8.GetString(request.downloadHandler.data);
                System.IO.StringReader ss = new System.IO.StringReader(str);

                int foundVersion = 0;
                long foundTimestamp = 0;
                string foundFileName = null;
                bool foundForce = false;

                string line = null;
                while ((line = ss.ReadLine()) != null)
                {
                    string[] infos = line.Split(" ");

                    if (infos.Length < 2)
                    {
                        continue;
                    }

                    bool enable = bool.Parse(infos[1]);

                    bool force = false;
                    if (infos.Length >= 3)
                    {
                        force = bool.Parse(infos[2]);
                    }

                    if (enable || WhiteDeviceMgr.isWhite())
                    {
                        int version = 0;
                        long timestamp = 0;
                        int min_app_version = 0;
                        int size = 0;
                        string md5 = null;
                        string fileName = infos[0];
                        ParseVersionInfoByFilePath(fileName, ref version, ref min_app_version, ref timestamp, ref size, ref md5);

                        if (min_app_version <= AppManager.GetAppInfo().hotupdate_version)
                        {
                            if (foundFileName == null || version > foundVersion || (version == foundVersion && timestamp > foundTimestamp))
                            {
                                foundVersion = version;
                                foundTimestamp = timestamp;
                                foundFileName = fileName;
                                foundForce = force;
                                break;
                            }
                        }
                    }
                }


                {
                    HotUpdateCodeInfo curInfo = useExternal ? externalInfo : localInfo;
                    bool needUpdate = curInfo == null;

                    if (foundForce)
                    {
                        needUpdate = true;
                    }
                    else
                    {
                        if (!needUpdate && foundFileName != null)
                        {
                            needUpdate = foundVersion > curInfo.hotupdate_version || (foundVersion == curInfo.hotupdate_version && foundTimestamp > curInfo.build_timestamp);
                        }
                    }

                    if (needUpdate)
                    {
                        if (externalInfo != null)
                        {
                            if (foundVersion == externalInfo.hotupdate_version && foundTimestamp == externalInfo.build_timestamp)
                            {
                                needUpdate = false;
                                useExternal = true;
                                if (foundForce)
                                {
                                    //save force version file
                                    string forceInfoFilePath = externalDirPath + "/" + force_info_file_name;
                                    CommonUtil.stringToFile(forceInfoFilePath, foundFileName);
                                }
                                else
                                {
                                    //delete force version file
                                    string forceInfoFilePath = externalDirPath + "/" + force_info_file_name;
                                    CommonUtil.Delete(forceInfoFilePath);
                                }
                            }
                        }

                        if (localInfo != null)
                        {
                            if (foundVersion == localInfo.hotupdate_version && foundTimestamp == localInfo.build_timestamp)
                            {
                                needUpdate = false;
                                useExternal = false;

                                //delete external version file and force version file
                                string[] externalFilePaths = System.IO.Directory.GetFiles(externalDirPath, "*", System.IO.SearchOption.TopDirectoryOnly);
                                foreach (var p in externalFilePaths)
                                {
                                    System.IO.File.Delete(p);
                                }
                            }
                        }
                    }

                    status = Status.version_checked;

                    if (needUpdate)
                    {
                        latestHotUpdateCodeFileName = foundFileName;
                        latestIsForce = foundForce;

                        if (!string.IsNullOrEmpty(latestHotUpdateCodeFileName))
                        {
                            int version = 0;
                            long timestamp = 0;
                            int min_app_version = 0;
                            int size = 0;
                            string md5 = null;

                            ParseVersionInfoByFilePath(latestHotUpdateCodeFileName, ref version, ref min_app_version, ref timestamp, ref size, ref md5);

                            latestInfo = new HotUpdateCodeInfo();
                            latestInfo.hotupdate_version = version;
                            latestInfo.build_timestamp = timestamp;
                            latestInfo.min_app_hotupdate_version = min_app_version;
                            downloadTotalSize = size;
                        }
                    }

                    yield return new object[] { request.result, needUpdate };
                }





            }
            else
            {
                status = Status.version_check_failed;
                yield return new object[] { request.result, false };
            }
        }


        public static IEnumerator Download()
        {
            if (status != Status.version_checked && status != Status.bundle_download_failed)
            {
                yield return UnityWebRequest.Result.DataProcessingError;
            }

            status = Status.bundle_downloading;

            if (latestHotUpdateCodeFileName == null)
            {
                status = Status.bundle_downladed;

                yield return UnityWebRequest.Result.Success;
            }

            string url = $"{host}/{hotupdate_code_dir}/{latestHotUpdateCodeFileName}";


            {
                int version = 0;
                long timestamp = 0;
                int min_app_version = 0;
                int size = 0;
                string md5 = null;

                ParseVersionInfoByFilePath(latestHotUpdateCodeFileName, ref version, ref min_app_version, ref timestamp, ref size, ref md5);
            }

            string localPath = externalDirPath + "/" + latestHotUpdateCodeFileName;
            localPath = localPath.Replace('\\', '/');

            var request = UnityWebRequest.Get(url);
            request.timeout = UnityWebRequestManager.timeout;
            request.downloadHandler = new DownloadHandlerFile(localPath);
            yield return request.SendWebRequest();

            if (request.result == UnityWebRequest.Result.Success)
            {
                externalBundlePath = localPath;
                if (externalInfo == null)
                {
                    externalInfo = new HotUpdateCodeInfo();
                }

                int version = 0;
                long timestamp = 0;
                int min_app_version = 0;
                int size = 0;
                string md5 = null;

                ParseVersionInfoByFilePath(latestHotUpdateCodeFileName, ref version, ref min_app_version, ref timestamp, ref size, ref md5);

                externalInfo.hotupdate_version = version;
                externalInfo.build_timestamp = timestamp;
                externalInfo.min_app_hotupdate_version = min_app_version;
                useExternal = true;

                string[] filePaths = System.IO.Directory.GetFiles(externalDirPath, "*", System.IO.SearchOption.TopDirectoryOnly);
                foreach (var p in filePaths)
                {
                    string fileName = System.IO.Path.GetFileName(p);
                    if (fileName != latestHotUpdateCodeFileName)
                    {
                        CommonUtil.Delete(p);
                    }
                }

                if (latestIsForce)
                {
                    //save force version file
                    string forceInfoFilePath = externalDirPath + "/" + force_info_file_name;
                    CommonUtil.stringToFile(forceInfoFilePath, latestHotUpdateCodeFileName);
                }

                status = Status.bundle_downladed;
            }
            else
            {
                status = Status.bundle_download_failed;
            }

            yield return request.result;
        }

        public static bool GetDownloadProgress(out int totalSize, out int progress)
        {
            totalSize = downloadTotalSize;
            if (downloadRequest != null)
            {
                float perent = (downloadRequest as UnityWebRequest).downloadProgress;
                progress = (int)(downloadTotalSize * perent);
                return true;
            }
            else
            {
                progress = 0;
                return false;
            }
        }


        public static HotUpdateCodeInfo LoadVersionInfoConfigFromBundle(AssetBundle ab)
        {
            var configAsset = ab.LoadAsset(config_file_name, typeof(TextAsset)) as TextAsset;
            if (configAsset == null)
            {
                return null;
            }

            return JsonConvert.DeserializeObject(configAsset.text, typeof(HotUpdateCodeInfo)) as HotUpdateCodeInfo;
        }

        static bool LoadCodeStep2(string[] dllNames, bool loadAotMeta, AssetBundle ab)
        {
            var info = LoadVersionInfoConfigFromBundle(ab);
            if (info == null)
            {
                Debug.LogError("load hotupdate code config failed: ");
                status = Status.code_load_failed;
                return false;
            }

            for (int i = 0; i < info.hotupdateDllNames.Length; i++)
            {
                string dllName = info.hotupdateDllNames[i];
                if (dllNames == null || System.Array.IndexOf(dllNames, dllName) >= 0)
                {
                    var dllAsset = ab.LoadAsset(dllName + ".bytes", typeof(TextAsset)) as TextAsset;

                    if (dllAsset == null)
                    {
                        Debug.LogError("load dll asset failed: " + dllName);
                        status = Status.code_load_failed;
                        return false;
                    }

                    var assembly = System.Reflection.Assembly.Load(dllAsset.bytes);
                }
            }

            if (loadAotMeta)
            {
                for (int i = 0; i < info.aotMetaDllNames.Length; i++)
                {
                    var dllAsset = ab.LoadAsset(info.aotMetaDllNames[i] + ".bytes", typeof(TextAsset)) as TextAsset;

                    if (dllAsset == null)
                    {
                        Debug.LogError("aot dll asset is null: " + info.aotMetaDllNames[i]);
                        status = Status.code_load_failed;
                        return false;
                    }

                    byte[] data = dllAsset.bytes;
                    if (loadMetadataForAOTAssemblyHandler != null)
                    {
                        try
                        {
                            if (!loadMetadataForAOTAssemblyHandler(data))
                            {
                                Debug.LogError("LoadMetadataForAOTAssembly  failed: " + info.aotMetaDllNames[i]);
                                status = Status.code_load_failed;
                                return false;
                            }
                        }
                        catch (System.Exception e)
                        {
                            Debug.LogException(e);

                            status = Status.code_load_failed;
                            return false;
                        }
                    }
                }
            }
            status = Status.code_loaded;

            return true;
        }




        public static IEnumerator LoadBundle()
        {
            if (codeBundle == null)
            {
                if (!useExternal)
                {
                    string configPath = UnityWebRequestManager.AddFilePrefixIfNeed(Application.streamingAssetsPath + "/" + hotupdate_code_dir + "/" + bundle_name);
                    var request = UnityWebRequest.Get(configPath);
                    request.timeout = UnityWebRequestManager.timeout;
                    yield return request.SendWebRequest();

                    codeBundle = AssetBundle.LoadFromMemory(request.downloadHandler.data);

                    yield return codeBundle != null;
                }
                else
                {
                    string configPath = UnityWebRequestManager.AddFilePrefixIfNeed(externalBundlePath);
                    var request = UnityWebRequest.Get(configPath);
                    request.timeout = UnityWebRequestManager.timeout;
                    yield return request.SendWebRequest();

                    codeBundle = AssetBundle.LoadFromMemory(request.downloadHandler.data);

                    yield return codeBundle != null;
                }
            }
            else
            {
                yield return true;
            }
        }



        public static IEnumerator LoadCode(string[] dllNames, bool loadAotMeta)
        {
            if (status != Status.inited && status != Status.version_checked && status != Status.bundle_downladed && status != Status.code_load_failed)
            {
                yield return false;
            }
            status = Status.code_loading;

            if (Application.isEditor)
            {
                status = Status.code_loaded;

                yield return true;
            }



            if (codeBundle != null)
            {
                bool ret = LoadCodeStep2(dllNames, loadAotMeta, codeBundle);

                yield return ret;
            }
            else
            {
                var e = LoadBundle().WithResult();
                yield return e;
                var result = (bool)e.Current;

                if (result)
                {
                    bool ret = LoadCodeStep2(dllNames, loadAotMeta, codeBundle);
                    yield return ret;
                }
                else
                {
                    yield return result;
                }
            }
        }


        public static AssetBundle GetBundle()
        {
            return codeBundle;
        }

        public static void UnloadBundle()
        {
            if (codeBundle == null)
            {
                return;
            }

            codeBundle.Unload(false);
            codeBundle = null;
        }

        public static string ToInfoString()
        {
            string info = "";
            info += "status: " + status + "\n";
            info += "host: " + host + "\n";
            // info += "isWhiteListDevice: " + isWhiteListDevice + "\n";
            info += "latestHotUpdateCodeFileName: " + latestHotUpdateCodeFileName + "\n";
            info += "externalBundlePath: " + externalBundlePath + "\n";

            if (externalInfo != null)
            {
                info += "externalInfo:\n" + externalInfo.ToString() + "\n";
            }

            if (localInfo != null)
            {
                info += "localInfo:\n" + localInfo.ToString() + "\n";
            }

            info += "useExternal:" + useExternal + "n";

            return info;
        }
    }
}

}